/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2020 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

/* jshint newcap: false */

"use strict";

var ui = angular.module('axelor.ui');

function TableLayout(items, attrs, $scope, $compile) {

  var colWidths = attrs.widths,
    numCols = +attrs.cols || 4,
    curCol = 0,
    layout = [[]];

  function add(item, label) {

    if (item.is('br')) {
      curCol = 0;
      item.hide();
      return layout.push([]);
    }

    var row = _.last(layout),
      cell = null,
      colspan = +item.attr('x-colspan') || 1,
      rowspan = +item.attr('x-rowspan') || 1;

    if (curCol + colspan >= numCols + 1) {
      curCol = 0, row = [];
      layout.push(row);
    }

    if (label) {
      cell = {};
      cell.elem = label;
      cell.css = label.attr('x-cell-css');
      row.push(cell);
      if (rowspan > 1) cell.rowspan = rowspan;
      if (colspan > 1) colspan -= 1;
      curCol += 1;
    }

    cell = {};
    cell.elem = item;
    cell.css = item.attr('x-cell-css');
    if (colspan > 1) cell.colspan = colspan;
    if (rowspan > 1) cell.rowspan = rowspan;

    row.push(cell);
    curCol += colspan;
  }

  if (colWidths && angular.isString(colWidths)) {
    colWidths = colWidths.trim().split(/\s*,\s*/);
    for(var i = 0 ; i < colWidths.length; i++) {
      var width = colWidths[i];
      if (/^(\d+)$/.test(width)) width = width + 'px';
      if (width == '*') width = 'auto';
      colWidths[i] = width;
    }
  }

  items.each(function(){
    var el = $(this),
      title = el.attr('x-title'),
      noTitle = el.attr('x-show-title') == 'false';

    var labelScope = el.data('$scope');
    if (labelScope) {
      labelScope = labelScope.$new();
    }

    if (numCols > 1 && !noTitle && title) {
      var label = $('<label ui-label></label>').html(title).attr('x-for-widget', el.attr('id')),
        labelElem = $compile(label)(labelScope || $scope);
      el.data('label', labelElem);
      return add(el, labelElem);
    }
    add(el);
  });

  var table = $('<table class="form-layout"></table>');

  function isLabel(cell) {
    return cell.css === "form-label" || (cell.elem && cell.elem.is('label,.spacer-item'));
  }

  function computeWidths(row) {
    if (row.length === 1) return null;
    var widths = [],
      labelCols = 0,
      itemCols = 0,
      emptyCols = 0;

    _.each(row, function(cell) {
      if (isLabel(cell)) {
        labelCols += (cell.colspan || 1);
      } else {
        itemCols += (cell.colspan || 1);
      }
    });

    emptyCols = numCols - (labelCols + itemCols);

    labelCols += (emptyCols / 2);
    itemCols += (emptyCols / 2) + (emptyCols % 2);

    var labelWidth = labelCols ? Math.min(50, (12 * labelCols)) / labelCols : 0;
    var itemWidth = (100 - (labelWidth * labelCols)) / itemCols;

    _.each(row, function(cell, i) {
      var width = ((isLabel(cell) ? labelWidth : itemWidth) * (cell.colspan || 1));
      widths[i] = width + "%";
    });

    return widths;
  }

  _.each(layout, function(row){
    var tr = $('<tr></tr>'),
      numCells = 0,
      widths = colWidths || computeWidths(row);

    _.each(row, function(cell, i) {
        var el = $('<td></td>')
          .addClass(cell.css)
          .attr('colspan', cell.colspan)
          .attr('rowspan', cell.rowspan)
          .append(cell.elem)
          .appendTo(tr);
        if (_.isArray(widths) && widths[i]) {
          el.width(widths[i]);
        }
        numCells += cell.colspan || 1;
    });

    // append remaining cells
    for (var i = numCells ; i < numCols ; i++) {
      $('<td></td>').appendTo(tr).width((widths||[])[i]);
    }

    tr.appendTo(table);
  });

  return table;
} //- TableLayout


ui.directive('uiTableLayout', ['$compile', function($compile) {

  return function(scope, element, attrs) {
    var elem = attrs.layoutSelector ? element.find(attrs.layoutSelector) : element;
    var items = elem.children();

    var layout = TableLayout(items, attrs, scope, $compile);
    var brTags = element.children('br:hidden'); // detach all the <br> tags

    scope.$on('$destroy', function(){
      brTags.remove();
    });

    elem.append(layout);
  };

}]);

function PanelLayout(items, attrs, $scope, $compile) {

  var stacked = attrs.stacked || false,
    flexbox = attrs.flexbox || false,
    numCols = 12,
    numSpan = +(attrs.itemSpan) || 6,
    curCol = 0,
    canAddRow = !stacked && !flexbox,
    rowClass = flexbox ? 'panel-flex' : 'row-fluid',
    cellClass = flexbox ? 'flex' : 'span',
    layout = [$('<div>').addClass(rowClass)];

  function add(item, label) {
    var row = _.last(layout),
      cell = $('<div>'),
      span = +item.attr('x-colspan') || numSpan,
      offset = +item.attr('x-coloffset') || 0;

    span = Math.min(span, numCols);
    if (stacked) {
      span = 0;
    }

    if (curCol + (span + offset) >= numCols + 1 && canAddRow) {
      curCol = 0, row = $('<div>').addClass(rowClass);
      layout.push(row);
    }
    if (label) {
      label.appendTo(cell);
      row.addClass('has-labels');
    }

    cell.addClass(item.attr('x-cell-css'));

    if (span) {
      cell.addClass(cellClass + span);
    }
    if (offset) {
      cell.addClass('offset' + offset);
    }

    cell.append(item);
    cell.appendTo(row);

    curCol += (span + offset);
  }

  items.each(function (item, i) {
    var el = $(this),
      title = el.attr('x-title'),
      noTitle = el.attr('x-show-title') == 'false';

    var labelScope = el.data('$scope');
    if (labelScope) {
      labelScope = labelScope.$new();
    }

    if (!noTitle && title) {
      var label = $('<label ui-label></label>').html(title).attr('x-for-widget', el.attr('id')),
        labelElem = $compile(label)(labelScope || $scope);
      el.data('label', labelElem);
      return add(el, labelElem);
    }
    add(el);
  });

  var container = $('<div class="panel-layout"></div>').append(layout);

  return container;
}

ui.directive('uiPanelLayout', ['$compile', function($compile) {

  return {
    priority: 1000,
    link: function(scope, element, attrs) {
      var elem = element.children('[ui-transclude]:first');
      var items = elem.children();
      var layout = PanelLayout(items, attrs, scope, $compile);
      elem.append(layout);
    }
  };

}]);

function BarLayout(items, attrs, $scope, $compile) {

  var main = $('<div class="bar-main">');
  var side = $('<div class="bar-side">');
  var wrap = $('<div class="bar-wrap">').appendTo(main);


  items.each(function(item, i) {
    var elem = $(this);
    var prop = elem.scope().field || {};
    if (elem.attr('x-sidebar')) {
      elem.appendTo(side);
    } else {
      elem.appendTo(wrap);
    }
    if (prop.attached) {
      elem.addClass("attached");
    }
  });

  var row = $('<div class="bar-container">').append(main);

  if (side && axelor.device.small) {
    side.children().first().prependTo(wrap);
    side.children().appendTo(wrap);
  }

  wrap.children('[ui-panel-mail]').appendTo(main);

  if (side.children().length > 0) {
    side.appendTo(row.addClass('has-side'));
  }

  return row;
}

ui.directive('uiBarLayout', ['$compile', function($compile) {

  return function(scope, element, attrs) {
    var items = element.children();
    var layout = BarLayout(items, attrs, scope, $compile);
    var schema = scope.schema || {};
    var css = null;

    scope._isPanelForm = true;

    element.append(layout);
    element.addClass('bar-layout');

    if (element.has('[x-sidebar]').length === 0) {
      css = "mid";
    }
    if (element.is('form') && ["mini", "mid", "large"].indexOf(schema.width) > -1) {
      css = scope.schema.width;
    }
    if (css) {
      element.addClass(css + '-form');
    }
  };
}]);

ui.directive('uiPanelViewer', function () {
  return {
    scope: true,
    link: function (scope, element, attrs) {
      var field = scope.field;
      var isRelational = /-to-one$/.test(field.type);
      if (isRelational) {
        Object.defineProperty(scope, 'record', {
          enumerable: true,
          get: function () {
            return (scope.$parent.record||{})[field.name];
          }
        });
      }
    }
  };
});

ui.directive('uiPanelEditor', ['$compile', 'ActionService', function($compile, ActionService) {

  return {
    scope: true,
    link: function(scope, element, attrs) {
      var field = scope.field;
      var editor = field.editor;

      if (!editor) {
        return;
      }

      function applyAttrs(item, level) {
        if (item.showTitle === undefined && !item.items) {
          item.showTitle = (editor.widgetAttrs||{}).showTitles !== "false";
        }
        if (!item.showTitle && !item.items) {
          var itemField = (editor.fields||scope.fields||{})[item.name] || {};
          item.placeholder = item.placeholder || itemField.placeholder || item.title || itemField.title || item.autoTitle;
        }
        if (editor.itemSpan && !item.colSpan && !level) {
          item.colSpan = editor.itemSpan;
        }
        if (item.items) {
          _.map(item.items, function (x) {
            applyAttrs(x, (level||0) + 1);
          });
        }
      }

      var items = editor.items || [];
      var hasColSpan = false;
      var widths = _.map(items, function (item) {
        applyAttrs(item);
        if (item.colSpan) {
          hasColSpan = true;
        }
        var width = item.width || (item.widgetAttrs||{}).width;
        return width ? width : (item.widget === 'toggle' ? 24 : '*');
      });

      var schema = hasColSpan ? {
        cols: 12,
        items: items
      } : {
        cols: items.length,
        colWidths: widths,
        items: items
      };

      if (editor.layout !== 'table') {
        schema = {
          items: [{
            type: 'panel',
            items: items,
            flexbox: editor.flexbox
          }]
        };
      }

      scope.fields = editor.fields || scope.fields;

      var form = ui.formBuild(scope, schema, scope.fields);
      var isRelational = /-to-one$/.test(field.type);

      if (isRelational) {
        Object.defineProperty(scope, 'record', {
          enumerable: true,
          get: function () {
            return (scope.$parent.record||{})[field.name];
          },
          set: function (value) {
            scope.setValue(value, true);
          }
        });
        Object.defineProperty(scope, '$$original', {
          enumerable: true,
          get: function () {
            return (scope.$parent.$$original||{})[field.name];
          },
          set: function (value) {}
        });
        scope.$$setEditorValue = function (value, fireOnChange) {
          scope.setValue(value, fireOnChange === undefined ? true: fireOnChange);
        };
      }

      if (field.target) {
        scope.getDummyValues = function() {
          if (!scope.record) return {};
          var fields = _.keys(scope.fields);
          var extra = _.chain(scope.fields_view)
                  .filter(function(f) { return f.name && f.name[0] === '$' && !_.contains(fields, f.name); })
                  .filter(function(f) { return ['$changed', '$editorModel', '$version', '$fetched', '$fetchedRelated'].indexOf(f) === -1; })
                  .pluck('name')
                  .compact()
                  .value();

          if (scope._model === 'com.axelor.auth.db.User') {
            extra = extra.filter(function (n) {
              return ['change', 'oldPassword', 'newPassword', 'chkPassword'].indexOf(n) === -1;
            });
          }

          return _.pick(scope.record, extra);
        };

        scope.getContext = function () {
          var context = _.extend({}, scope.record);
          var dummy = scope.getDummyValues();
          context._model = scope._model;
          context._parent = scope.$parent.getContext();
          return ui.prepareContext(scope._model, context, dummy);
        };

        scope.$broadcastRecordChange = function () {
          scope.$broadcast("on:record-change", scope.record);
        };

        scope.$on('on:before-save', function watchParentRecord() {
          var dummyValues = scope.getDummyValues();
          var watcher = scope.$watch('$parent.record', function (record, old) {
            if (record === old) return;
            var value = (record||{})[field.name];
            if (value && dummyValues) {
              value = _.extend(value, dummyValues);
            }
            dummyValues = null;
            watcher();
          });
        });

        scope.$watch('record', function (record, old) {
          if (record && !record.$editorModel) {
            record.$editorModel = scope._model;
          }
        });

        // make sure to fetch missing values
        var fetchMissing = function (value) {
          var ds = scope._dataSource;
          var record = scope.record;
          if (value <= 0 || !value || record.$fetched || record.$fetchedRelated) {
            return;
          }
          var missing = _.filter(_.keys(editor.fields), function (name) {
            if (!record) return false;
            if (name.indexOf('.') === -1) {
              return !record.hasOwnProperty(name);
            }
            var path = name.split('.');
            var nested = record;
            for (var i = 0; i < path.length - 1; i++) {
              nested = nested[path[i]];
              if (!nested) {
                return false;
              }
            }
            return !nested.hasOwnProperty(path[path.length - 1]);
          });
          if (missing.length === 0) {
            return;
          }
          record.$fetchedRelated = true;
          return ds.read(value, {fields: missing}).success(function(rec) {
            var values = _.pick(rec, missing);
            record = _.extend(record, values);
          });
        };
        // make sure to trigger record-change with proper record data
        var watchRun = function (value, old) {
          if (value && value !== old) {
            value.$changed = true;
            value.version = _.isNumber(value.version) ? value.version : value.$version;
          }
          if (value) {
            // parent form's getContext will check this to prepare context for editor
            // to have proper selection flags in nest o2m/m2m
            value.$editorModel = scope._model;
            fetchMissing(value.id);
          }
          scope.$applyAsync(function () {
            scope.$broadcast("on:record-change", value || {}, true);
          });
          // if it's an o2m editor, make sure to update values
          if (scope.$itemsChanged) {
            scope.$itemsChanged();
          }
        };
        scope.$watch('record', _.debounce(watchRun, 100), true);
        scope.$timeout(function () {
          scope.$broadcast("on:record-change", scope.record || {}, true);
        });
      }

      form = $compile(form)(scope);
      form.removeClass('mid-form mini-form').children('div.row').removeClass('row').addClass('row-fluid');
      element.append(form);

      if (field.target) {
        var handler = null;
        if (editor.onNew) {
          schema.onNew = editor.onNew;
          form.data('$editorForm', form);
          handler = ActionService.handler(scope, form, {
            action: editor.onNew
          });
        }
        scope.$watch('record.id', function editorRecordIdWatch(value, old) {
          if (!value && handler) {
            handler.onNew();
          }
        });
      }

      scope.isValid = function () {
        return scope.form && scope.form.$valid;
      };

      function isEmpty(record) {
        if (!record || _.isEmpty(record)) return true;
        var values = _.filter(record, function (value, name) {
          return !(/[\$_]/.test(name) || value === null || value === undefined);
        });
        return values.length === 0;
      }

      scope.$watch(function editorValidWatch() {
        if (isRelational && editor.showOnNew === false && !scope.canShowEditor()) {
          return;
        }
        var valid = scope.isValid();
        if (!valid && !field.jsonFields && !scope.$parent.isRequired() && isEmpty(scope.record)) {
          var errors = (scope.form || {}).$error || {};
          valid = !errors.valid;
        }
        if (scope.setValidity) {
          scope.setValidity('valid', valid, scope.record);
          element.toggleClass('nested-not-required', valid);
        } else {
          scope.$parent.form.$setValidity('valid', valid, scope.form);
        }
      });

      scope.$on('$destroy', function () {
        if (scope.setValidity) {
          scope.setValidity('valid', true);
        }
      });
    }
  };
}]);

})();
